/** @file projection.cpp
    @brief OpenGL projections */

#include "projection.h"

using namespace G3;

//==============================================================================
// Constructors and Destructors
//==============================================================================
G3::RENDER_TO_TEXTURE::RENDER_TO_TEXTURE () {
	TextureDimensions.Left = 0;
	TextureDimensions.Top = 0;
	TextureDimensions.Right = 256;
	TextureDimensions.Bottom = 256;
	TextureTargetID = 0;
	Format = GL_RGBA;
	LevelOfDetail = 0;
	fboID = 0;
	log = &G3::MainLog;
}

G3::RENDER_TO_TEXTURE::~RENDER_TO_TEXTURE () {
    if (useFBO && GLEE_EXT_framebuffer_object) {
        glDeleteFramebuffersEXT (1, &fboID);
        glDeleteRenderbuffersEXT (1, &fboDepthID);
    }
}

//==============================================================================
// Initializes the render2texture object
//==============================================================================
void G3::RENDER_TO_TEXTURE::Init  () {
    // Are PBO's supported ?
    if (GLEE_EXT_framebuffer_object) {
        if (useFBO) {
            // Generate a framebuffer
            glGenFramebuffersEXT (1, &fboID);
            glErrCheck (log, "glGenFramebuffersEXT", FILE_LINE);

            // Bind it
            glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, fboID);
            glErrCheck (log, "glBindFramebufferEXT", FILE_LINE);

            // Generate a renderbuffer
            glGenRenderbuffersEXT (1, &fboDepthID);
            glErrCheck (log, "glGenRenderbuffersEXT", FILE_LINE);

            // Bind it
            glBindRenderbufferEXT (GL_RENDERBUFFER_EXT, fboDepthID);
            glErrCheck (log, "glBindRenderbufferEXT", FILE_LINE);

            // Allocate the buffer
            glRenderbufferStorageEXT (GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT, TextureDimensions.Right - TextureDimensions.Left, TextureDimensions.Bottom - TextureDimensions.Top);
            glErrCheck (log, "glRenderbufferStorageEXT", FILE_LINE);

            // Attach the renderbuffer to the FBO
            glFramebufferRenderbufferEXT (GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, fboDepthID);
            glErrCheck (log, "glFramebufferRenderbufferEXT", FILE_LINE);
        }
    } else {
        if (log)
            log->Report ("Warning, Frame Buffer Objects are not supported, falling back to slower render to texture methods..\n");
    }
}

//==============================================================================
// Selects a target texture
//==============================================================================
void G3::RENDER_TO_TEXTURE::SelectTexture (uint TextureID) {
	TextureTargetID = TextureID;
}

//==============================================================================
// Selects target texture dimensions according to the current preferences
//==============================================================================
void G3::RENDER_TO_TEXTURE::glSelectDimensions () {
	int ViewPort [4] = {0, 0, 0, 0};
	glGetIntegerv (GL_VIEWPORT, ViewPort);

	ViewportDimensions.Left = ViewPort [0];
	ViewportDimensions.Top = ViewPort [1];
	ViewportDimensions.Right = ViewPort [2] + ViewportDimensions.Left;
	ViewportDimensions.Bottom = ViewPort [3] + ViewportDimensions.Top;

	if (ViewportDimensions == TextureDimensions)
        return;

	glViewport (TextureDimensions.Left, TextureDimensions.Top, TextureDimensions.Right - TextureDimensions.Left, TextureDimensions.Bottom - TextureDimensions.Top);
}

//==============================================================================
// Selects target texture dimensions and sets them as current preferences
//==============================================================================
void G3::RENDER_TO_TEXTURE::glSelectDimensions (G3::RECTI TextureDim) {
	int ViewPort [4] = {0, 0, 0, 0};
	glGetIntegerv (GL_VIEWPORT, ViewPort);

	ViewportDimensions.Left = ViewPort [0];
	ViewportDimensions.Top = ViewPort [1];
	ViewportDimensions.Right = ViewPort [2] + ViewportDimensions.Left;
	ViewportDimensions.Bottom = ViewPort [3] + ViewportDimensions.Top;

	TextureDimensions = TextureDim;

	if (ViewportDimensions == TextureDimensions)
        return;

	glViewport (TextureDimensions.Left, TextureDimensions.Top, TextureDim.Right - TextureDim.Left, TextureDim.Bottom - TextureDim.Top);
}

//==============================================================================
// Renders to the texture
//
//		uint TextureID				Target Texture ID
//				UINT_MAX		Selects the default/last used target texture
//
//		GLenum Format				Any OpenGL pixel format
//				GL_RGBA			This is the default
//
//		int LevelOfDetail			The Mip level
//				0			This is the default
//==============================================================================
void G3::RENDER_TO_TEXTURE::Begin (uint TextureID, GLenum aFormat, int aLevelOfDetail) {
	uint TexID = TextureID;
	int error = 0;

	if (TextureID != UINT_MAX)
		TextureTargetID = TexID;

    if (useFBO && GLEE_EXT_framebuffer_object) {
        glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, fboID);
        glErrCheck (log, "glBindFramebufferEXT", FILE_LINE);

        glFramebufferTexture2DEXT (GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, TextureTargetID, 0);
        glErrCheck (log, "glFramebufferTexture2DEXT", FILE_LINE);

        if (glCheckFramebufferStatusEXT (GL_FRAMEBUFFER_EXT) != GL_FRAMEBUFFER_COMPLETE_EXT) {
            if (log)
                log->Report ("RENDER_TO_TEXTURE::Begin (glCheckFramebufferStatusEXT): The frame buffer is not complete for rendering..\n");
        }
    }

    glSelectDimensions ();

    Format = aFormat;
    LevelOfDetail = aLevelOfDetail;
}

void G3::RENDER_TO_TEXTURE::End () {
    int error = 0;

    if (useFBO && GLEE_EXT_framebuffer_object) {
        glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, 0);
        glErrCheck (log, "glBindFramebufferEXT", FILE_LINE);
    } else {
        glBindTexture (GL_TEXTURE_2D, TextureTargetID);

        glCopyTexImage2D (GL_TEXTURE_2D, LevelOfDetail, Format, TextureDimensions.Left, ViewportDimensions.Bottom - TextureDimensions.Bottom, TextureDimensions.Right - TextureDimensions.Left, TextureDimensions.Bottom - TextureDimensions.Top, 0);
        // This should be a bit faster:
        //glCopyTexSubImage2D (GL_TEXTURE_2D, LevelOfDetail, 0, 0, TextureDimensions.Left, TextureDimensions.Top, TextureDimensions.Right - TextureDimensions.Left, TextureDimensions.Bottom - TextureDimensions.Top);

        glErrCheck (log, "glCopyTexImage2D", FILE_LINE);
    }

    glViewport (ViewportDimensions.Left, ViewportDimensions.Top, ViewportDimensions.Right - ViewportDimensions.Left, ViewportDimensions.Bottom - ViewportDimensions.Top);
}

//==============================================================================
// A replacement of gluPerspective
//==============================================================================
void G3::glPerspective (double FieldOfView, double AspectRatio, double zNear, double zFar) {
	double hW = 0.0, hH = 0.0;

	hH = tan ((FieldOfView / 2.0) / 180.0 * G3_PI) * zNear;
	hW = hH * AspectRatio;

	glFrustum (-hW, hW, -hH, hH, zNear, zFar);
}

void G3::glPerspective (FRUSTUM *Frustum) {
	double hW = 0.0, hH = 0.0;

	Assert (Frustum, "glPerspective: Frustum argument is a NULL pointer");

	hH = tan ((Frustum->FieldOfView / 2.0) / 180.0 * G3_PI) * Frustum->NearPlane;
	hW = hH * Frustum->AspectRatio;

	glFrustum (-hW, hW, -hH, hH, Frustum->NearPlane, Frustum->FarPlane);
}

//==============================================================================
// Sets OpenGL perspective
//==============================================================================
void G3::glSetPerspective (const RECTI &View, float Near, float Far, float FieldOfView) {
    //Set up a perspective
    glViewport (View.Left, View.Top, View.Right, View.Bottom);

    glMatrixMode (GL_PROJECTION);

	    glLoadIdentity ();
	    // Calculate The Aspect Ratio Of The Window
	    glPerspective (FieldOfView, (View.Right - View.Left) / (View.Bottom - View.Top), Near, Far);

    glMatrixMode (GL_MODELVIEW);

    glLoadIdentity ();
}

// NOTE:: Also affects the frustum
void G3::glSetPerspective (const RECTI &View, FRUSTUM *Frustum) {
	Assert (Frustum, "glSetPerspective: Frustum argument is a NULL pointer");

	//Set up a perspective
	glViewport (View.Left, View.Top, View.Right, View.Bottom);
	// Just in case user has not set it yet
	Frustum->SetAspectRatio ((uint) (View.Right - View.Left), (uint) (View.Bottom - View.Top));

	glMatrixMode (GL_PROJECTION);

	    glLoadIdentity ();

	    glPerspective (Frustum);

	glMatrixMode (GL_MODELVIEW);

	glLoadIdentity ();
}

//==============================================================================
// Returns to OpenGL perspective
//==============================================================================
void G3::glRestorePerspective () {
	glMatrixMode (GL_PROJECTION);
	glPopMatrix ();
	glMatrixMode (GL_MODELVIEW);
	glPopMatrix ();
}

//==============================================================================
// Sets to OpenGL 2D projection
//==============================================================================
void G3::glSetPlanar (const RECTI &View, float Near, float Far) {
	glMatrixMode (GL_PROJECTION);
		glPushMatrix ();
		glLoadIdentity ();

		glOrtho (View.Left, View.Right, View.Bottom, View.Top, Near, Far);
	glMatrixMode (GL_MODELVIEW);

	glPushMatrix ();
	glLoadIdentity ();
}

void G3::glSetPlanar (float Near, float Far) {
	// Get the viewport coordinates
	int ViewPort [4];
	glGetIntegerv (GL_VIEWPORT, ViewPort);

	glMatrixMode (GL_PROJECTION);
		glPushMatrix ();
		glLoadIdentity ();

		glOrtho (0, ViewPort [2], 0, ViewPort [3], Near, Far);
	glMatrixMode (GL_MODELVIEW);

	glPushMatrix ();
	glLoadIdentity ();
}

void G3::glSetPlanar (const RECTI &View, FRUSTUM *Frustum) {
	Assert (Frustum, "glSetPlanar: Frustum argument is a NULL pointer");

	glMatrixMode (GL_PROJECTION);
		glPushMatrix ();
		glLoadIdentity ();

		glOrtho (View.Left, View.Right, View.Bottom, View.Top, Frustum->NearPlane, Frustum->FarPlane);
	glMatrixMode (GL_MODELVIEW);

	glPushMatrix ();
	glLoadIdentity ();
}

//==============================================================================
// Projects a 2I vector
//==============================================================================
RAY3D G3::ProjectPerspective (const VEC2I &Vector) {
	RAY3D Res;
	VEC2D Vec2;

	double mvmatrix [16];
	double projmatrix [16];
	int viewport [4];

	glGetIntegerv (GL_VIEWPORT, viewport);
	glGetDoublev (GL_MODELVIEW_MATRIX, mvmatrix);
	glGetDoublev (GL_PROJECTION_MATRIX, projmatrix);

	Vec2.x = double (Vector.x);
	Vec2.y = double (viewport [3] - viewport [1] - Vector.y);

	// Near plane (screen Z = 0)
	gluUnProject (Vec2.x, Vec2.y, 0.0, mvmatrix, projmatrix, viewport, &Res.a.x, &Res.a.y, &Res.a.z);
	// Far plane (screen Z = 1)
	gluUnProject (Vec2.x, Vec2.y, 1.0, mvmatrix, projmatrix, viewport, &Res.b.x, &Res.b.y, &Res.b.z);

	return Res;
}
